/***
 * ASM XML Adapter
 * Copyright (c) 2004-2011, Eugene Kuleshov
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the copyright holders nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.objectweb.asm.xml;

import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

import javax.xml.transform.Source;
import javax.xml.transform.Templates;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.transform.sax.TransformerHandler;
import javax.xml.transform.stream.StreamSource;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.ext.LexicalHandler;
import org.xml.sax.helpers.AttributesImpl;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;

/**
 * Processor is a command line tool that can be used for bytecode waving
 * directed by XSL transformation.
 * <p>
 * In order to use a concrete XSLT engine, system property
 * <tt>javax.xml.transform.TransformerFactory</tt> must be set to one of the
 * following values.
 * 
 * <blockquote>
 * <table border="1" cellspacing="0" cellpadding="3">
 * <tr>
 * <td>jd.xslt</td>
 * <td>jd.xml.xslt.trax.TransformerFactoryImpl</td>
 * </tr>
 * 
 * <tr>
 * <td>Saxon</td>
 * <td>net.sf.saxon.TransformerFactoryImpl</td>
 * </tr>
 * 
 * <tr>
 * <td>Caucho</td>
 * <td>com.caucho.xsl.Xsl</td>
 * </tr>
 * 
 * <tr>
 * <td>Xalan interpeter</td>
 * <td>org.apache.xalan.processor.TransformerFactory</td>
 * </tr>
 * 
 * <tr>
 * <td>Xalan xsltc</td>
 * <td>org.apache.xalan.xsltc.trax.TransformerFactoryImpl</td>
 * </tr>
 * </table>
 * </blockquote>
 * 
 * @author Eugene Kuleshov
 */
public class Processor {

  public static final int BYTECODE = 1;

  public static final int MULTI_XML = 2;

  public static final int SINGLE_XML = 3;

  private static final String SINGLE_XML_NAME = "classes.xml";

  private final int inRepresentation;

  private final int outRepresentation;

  private final InputStream input;

  private final OutputStream output;

  private final Source xslt;

  private int n = 0;

  public Processor(final int inRepresenation, final int outRepresentation, final InputStream input, final OutputStream output, final Source xslt) {
    this.inRepresentation = inRepresenation;
    this.outRepresentation = outRepresentation;
    this.input = input;
    this.output = output;
    this.xslt = xslt;
  }

  public int process() throws TransformerException, IOException, SAXException {
    ZipInputStream zis = new ZipInputStream(input);
    final ZipOutputStream zos = new ZipOutputStream(output);
    final OutputStreamWriter osw = new OutputStreamWriter(zos);

    Thread.currentThread().setContextClassLoader(getClass().getClassLoader());

    TransformerFactory tf = TransformerFactory.newInstance();
    if (!tf.getFeature(SAXSource.FEATURE) || !tf.getFeature(SAXResult.FEATURE)) {
      return 0;
    }

    SAXTransformerFactory saxtf = (SAXTransformerFactory) tf;
    Templates templates = null;
    if (xslt != null) {
      templates = saxtf.newTemplates(xslt);
    }

    // configuring outHandlerFactory
    // ///////////////////////////////////////////////////////

    EntryElement entryElement = getEntryElement(zos);

    ContentHandler outDocHandler = null;
    switch (outRepresentation) {
    case BYTECODE:
      outDocHandler = new OutputSlicingHandler(new ASMContentHandlerFactory(zos), entryElement, false);
      break;

    case MULTI_XML:
      outDocHandler = new OutputSlicingHandler(new SAXWriterFactory(osw, true), entryElement, true);
      break;

    case SINGLE_XML:
      ZipEntry outputEntry = new ZipEntry(SINGLE_XML_NAME);
      zos.putNextEntry(outputEntry);
      outDocHandler = new SAXWriter(osw, false);
      break;

    }

    // configuring inputDocHandlerFactory
    // /////////////////////////////////////////////////
    ContentHandler inDocHandler;
    if (templates == null) {
      inDocHandler = outDocHandler;
    } else {
      inDocHandler = new InputSlicingHandler("class", outDocHandler, new TransformerHandlerFactory(saxtf, templates, outDocHandler));
    }
    ContentHandlerFactory inDocHandlerFactory = new SubdocumentHandlerFactory(inDocHandler);

    if (inDocHandler != null && inRepresentation != SINGLE_XML) {
      inDocHandler.startDocument();
      inDocHandler.startElement("", "classes", "classes", new AttributesImpl());
    }

    int i = 0;
    ZipEntry ze;
    while ((ze = zis.getNextEntry()) != null) {
      update(ze.getName(), n++);
      if (isClassEntry(ze)) {
        processEntry(zis, ze, inDocHandlerFactory);
      } else {
        OutputStream os = entryElement.openEntry(getName(ze));
        copyEntry(zis, os);
        entryElement.closeEntry();
      }

      i++;
    }

    if (inDocHandler != null && inRepresentation != SINGLE_XML) {
      inDocHandler.endElement("", "classes", "classes");
      inDocHandler.endDocument();
    }

    if (outRepresentation == SINGLE_XML) {
      zos.closeEntry();
    }
    zos.flush();
    zos.close();

    return i;
  }

  private void copyEntry(final InputStream is, final OutputStream os) throws IOException {
    if (outRepresentation == SINGLE_XML) {
      return;
    }

    byte[] buff = new byte[2048];
    int i;
    while ((i = is.read(buff)) != -1) {
      os.write(buff, 0, i);
    }
  }

  private boolean isClassEntry(final ZipEntry ze) {
    String name = ze.getName();
    return inRepresentation == SINGLE_XML && name.equals(SINGLE_XML_NAME) || name.endsWith(".class") || name.endsWith(".class.xml");
  }

  private void processEntry(final ZipInputStream zis, final ZipEntry ze, final ContentHandlerFactory handlerFactory) {
    ContentHandler handler = handlerFactory.createContentHandler();
    try {

      // if (CODE2ASM.equals(command)) { // read bytecode and process it
      // // with TraceClassVisitor
      // ClassReader cr = new ClassReader(readEntry(zis, ze));
      // cr.accept(new TraceClassVisitor(null, new PrintWriter(os)),
      // false);
      // }

      boolean singleInputDocument = inRepresentation == SINGLE_XML;
      if (inRepresentation == BYTECODE) { // read bytecode and process it
        // with handler
        ClassReader cr = new ClassReader(readEntry(zis, ze));
        cr.accept(new SAXClassAdapter(handler, singleInputDocument), 0);

      } else { // read XML and process it with handler
        XMLReader reader = XMLReaderFactory.createXMLReader();
        reader.setContentHandler(handler);
        reader
            .parse(new InputSource(singleInputDocument ? (InputStream) new ProtectedInputStream(zis) : new ByteArrayInputStream(readEntry(zis, ze))));

      }
    } catch (Exception ex) {
      update(ze.getName(), 0);
      update(ex, 0);
    }
  }

  private EntryElement getEntryElement(final ZipOutputStream zos) {
    if (outRepresentation == SINGLE_XML) {
      return new SingleDocElement(zos);
    }
    return new ZipEntryElement(zos);
  }

  // private ContentHandlerFactory getHandlerFactory(
  // OutputStream os,
  // SAXTransformerFactory saxtf,
  // Templates templates)
  // {
  // ContentHandlerFactory factory = null;
  // if (templates == null) {
  // if (outputRepresentation == BYTECODE) { // factory used to write
  // // bytecode
  // factory = new ASMContentHandlerFactory(os, computeMax);
  // } else { // factory used to write XML
  // factory = new SAXWriterFactory(os, true);
  // }
  // } else {
  // if (outputRepresentation == BYTECODE) { // factory used to transform
  // // and then write bytecode
  // factory = new ASMTransformerHandlerFactory(saxtf,
  // templates,
  // os,
  // computeMax);
  // } else { // factory used to transformand then write XML
  // factory = new TransformerHandlerFactory(saxtf,
  // templates,
  // os,
  // outputRepresentation == SINGLE_XML);
  // }
  // }
  // return factory;
  // }

  private String getName(final ZipEntry ze) {
    String name = ze.getName();
    if (isClassEntry(ze)) {
      if (inRepresentation != BYTECODE && outRepresentation == BYTECODE) {
        name = name.substring(0, name.length() - 4); // .class.xml to
        // .class
      } else if (inRepresentation == BYTECODE && outRepresentation != BYTECODE) {
        name += ".xml"; // .class to .class.xml
      }
      // } else if( CODE2ASM.equals( command)) {
      // name = name.substring( 0, name.length()-6).concat( ".asm");
    }
    return name;
  }

  private static byte[] readEntry(final InputStream zis, final ZipEntry ze) throws IOException {
    long size = ze.getSize();
    if (size > -1) {
      byte[] buff = new byte[(int) size];
      int k = 0;
      int n;
      while ((n = zis.read(buff, k, buff.length - k)) > 0) {
        k += n;
      }
      return buff;
    }

    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    byte[] buff = new byte[4096];
    int i;
    while ((i = zis.read(buff)) != -1) {
      bos.write(buff, 0, i);
    }
    return bos.toByteArray();
  }

  /*
   * (non-Javadoc)
   * 
   * @see java.util.Observer#update(java.util.Observable, java.lang.Object)
   */
  protected void update(final Object arg, final int n) {
    if (arg instanceof Throwable) {
      ((Throwable) arg).printStackTrace();
    } else {
      if (n % 100 == 0) {
        System.err.println(n + " " + arg);
      }
    }
  }

  public static void main(final String[] args) throws Exception {
    if (args.length < 2) {
      showUsage();
      return;
    }

    int inRepresentation = getRepresentation(args[0]);
    int outRepresentation = getRepresentation(args[1]);

    InputStream is = System.in;
    OutputStream os = new BufferedOutputStream(System.out);

    Source xslt = null;
    // boolean computeMax = true;

    for (int i = 2; i < args.length; i++) {
      if ("-in".equals(args[i])) {
        is = new FileInputStream(args[++i]);

      } else if ("-out".equals(args[i])) {
        os = new BufferedOutputStream(new FileOutputStream(args[++i]));

      } else if ("-xslt".equals(args[i])) {
        xslt = new StreamSource(new FileInputStream(args[++i]));

        // } else if( "-computemax".equals( args[ i].toLowerCase())) {
        // computeMax = true;

      } else {
        showUsage();
        return;

      }
    }

    if (inRepresentation == 0 || outRepresentation == 0) {
      showUsage();
      return;
    }

    Processor m = new Processor(inRepresentation, outRepresentation, is, os, xslt);

    long l1 = System.currentTimeMillis();
    int n = m.process();
    long l2 = System.currentTimeMillis();
    System.err.println(n);
    System.err.println((l2 - l1) + "ms  " + 1000f * n / (l2 - l1) + " resources/sec");
  }

  private static int getRepresentation(final String s) {
    if ("code".equals(s)) {
      return BYTECODE;
    } else if ("xml".equals(s)) {
      return MULTI_XML;
    } else if ("singlexml".equals(s)) {
      return SINGLE_XML;
    }
    return 0;
  }

  private static void showUsage() {
    System.err.println("Usage: Main <in format> <out format> [-in <input jar>] [-out <output jar>] [-xslt <xslt fiel>]");
    System.err.println("  when -in or -out is omitted sysin and sysout would be used");
    System.err.println("  <in format> and <out format> - code | xml | singlexml");
  }

  /**
   * IputStream wrapper class used to protect input streams from being closed by
   * some stupid XML parsers.
   */
  private static final class ProtectedInputStream extends InputStream {
    private final InputStream is;

    ProtectedInputStream(final InputStream is) {
      this.is = is;
    }

    @Override
    public final void close() throws IOException {
    }

    @Override
    public final int read() throws IOException {
      return is.read();
    }

    @Override
    public final int read(final byte[] b, final int off, final int len) throws IOException {
      return is.read(b, off, len);
    }

    @Override
    public final int available() throws IOException {
      return is.available();
    }
  }

  /**
   * A {@link ContentHandlerFactory ContentHandlerFactory} is used to create
   * {@link org.xml.sax.ContentHandler ContentHandler} instances for concrete
   * context.
   */
  private static interface ContentHandlerFactory {

    /**
     * Creates an instance of the content handler.
     * 
     * @return content handler
     */
    ContentHandler createContentHandler();

  }

  /**
   * SAXWriterFactory
   */
  private static final class SAXWriterFactory implements ContentHandlerFactory {
    private final Writer w;

    private final boolean optimizeEmptyElements;

    SAXWriterFactory(final Writer w, final boolean optimizeEmptyElements) {
      this.w = w;
      this.optimizeEmptyElements = optimizeEmptyElements;
    }

    public final ContentHandler createContentHandler() {
      return new SAXWriter(w, optimizeEmptyElements);
    }

  }

  /**
   * ASMContentHandlerFactory
   */
  private static final class ASMContentHandlerFactory implements ContentHandlerFactory {
    final OutputStream os;

    ASMContentHandlerFactory(final OutputStream os) {
      this.os = os;
    }

    public final ContentHandler createContentHandler() {
      final ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
      return new ASMContentHandler(cw) {
        @Override
        public void endDocument() throws SAXException {
          try {
            os.write(cw.toByteArray());
          } catch (IOException e) {
            throw new SAXException(e);
          }
        }
      };
    }

  }

  /**
   * TransformerHandlerFactory
   */
  private static final class TransformerHandlerFactory implements ContentHandlerFactory {
    private SAXTransformerFactory saxtf;

    private final Templates templates;

    private ContentHandler outputHandler;

    TransformerHandlerFactory(final SAXTransformerFactory saxtf, final Templates templates, final ContentHandler outputHandler) {
      this.saxtf = saxtf;
      this.templates = templates;
      this.outputHandler = outputHandler;
    }

    public final ContentHandler createContentHandler() {
      try {
        TransformerHandler handler = saxtf.newTransformerHandler(templates);
        handler.setResult(new SAXResult(outputHandler));
        return handler;
      } catch (TransformerConfigurationException ex) {
        throw new RuntimeException(ex.toString());
      }
    }
  }

  /**
   * SubdocumentHandlerFactory
   */
  private static final class SubdocumentHandlerFactory implements ContentHandlerFactory {
    private final ContentHandler subdocumentHandler;

    SubdocumentHandlerFactory(final ContentHandler subdocumentHandler) {
      this.subdocumentHandler = subdocumentHandler;
    }

    public final ContentHandler createContentHandler() {
      return subdocumentHandler;
    }

  }

  /**
   * A {@link org.xml.sax.ContentHandler ContentHandler} and
   * {@link org.xml.sax.ext.LexicalHandler LexicalHandler} that serializes XML
   * from SAX 2.0 events into {@link java.io.Writer Writer}.
   * 
   * <i><blockquote> This implementation does not support namespaces, entity
   * definitions (uncluding DTD), CDATA and text elements. </blockquote></i>
   */
  private static final class SAXWriter extends DefaultHandler implements LexicalHandler {
    private static final char[] OFF = "                                                                                                        "
        .toCharArray();

    private Writer w;

    private final boolean optimizeEmptyElements;

    private boolean openElement = false;

    private int ident = 0;

    /**
     * Creates <code>SAXWriter</code>.
     * 
     * @param w
     *          writer
     * @param optimizeEmptyElements
     *          if set to <code>true</code>, short XML syntax will be used for
     *          empty elements
     */
    SAXWriter(final Writer w, final boolean optimizeEmptyElements) {
      this.w = w;
      this.optimizeEmptyElements = optimizeEmptyElements;
    }

    @Override
    public final void startElement(final String ns, final String localName, final String qName, final Attributes atts) throws SAXException {
      try {
        closeElement();

        writeIdent();
        w.write('<' + qName);
        if (atts != null && atts.getLength() > 0) {
          writeAttributes(atts);
        }

        if (optimizeEmptyElements) {
          openElement = true;
        } else {
          w.write(">\n");
        }
        ident += 2;

      } catch (IOException ex) {
        throw new SAXException(ex);

      }
    }

    @Override
    public final void endElement(final String ns, final String localName, final String qName) throws SAXException {
      ident -= 2;
      try {
        if (openElement) {
          w.write("/>\n");
          openElement = false;
        } else {
          writeIdent();
          w.write("</" + qName + ">\n");
        }

      } catch (IOException ex) {
        throw new SAXException(ex);

      }
    }

    @Override
    public final void endDocument() throws SAXException {
      try {
        w.flush();

      } catch (IOException ex) {
        throw new SAXException(ex);

      }
    }

    public final void comment(final char[] ch, final int off, final int len) throws SAXException {
      try {
        closeElement();

        writeIdent();
        w.write("<!-- ");
        w.write(ch, off, len);
        w.write(" -->\n");

      } catch (IOException ex) {
        throw new SAXException(ex);

      }
    }

    public final void startDTD(final String arg0, final String arg1, final String arg2) throws SAXException {
    }

    public final void endDTD() throws SAXException {
    }

    public final void startEntity(final String arg0) throws SAXException {
    }

    public final void endEntity(final String arg0) throws SAXException {
    }

    public final void startCDATA() throws SAXException {
    }

    public final void endCDATA() throws SAXException {
    }

    private final void writeAttributes(final Attributes atts) throws IOException {
      StringBuilder sb = new StringBuilder();
      int len = atts.getLength();
      for (int i = 0; i < len; i++) {
        sb.append(' ').append(atts.getLocalName(i)).append("=\"").append(esc(atts.getValue(i))).append('\"');
      }
      w.write(sb.toString());
    }

    /**
     * Encode string with escaping.
     * 
     * @param str
     *          string to encode.
     * @return encoded string
     */
    private static final String esc(final String str) {
      StringBuilder sb = new StringBuilder(str.length());
      for (int i = 0; i < str.length(); i++) {
        char ch = str.charAt(i);
        switch (ch) {
        case '&':
          sb.append("&amp;");
          break;

        case '<':
          sb.append("&lt;");
          break;

        case '>':
          sb.append("&gt;");
          break;

        case '\"':
          sb.append("&quot;");
          break;

        default:
          if (ch > 0x7f) {
            sb.append("&#").append(Integer.toString(ch)).append(';');
          } else {
            sb.append(ch);
          }

        }
      }
      return sb.toString();
    }

    private final void writeIdent() throws IOException {
      int n = ident;
      while (n > 0) {
        if (n > OFF.length) {
          w.write(OFF);
          n -= OFF.length;
        } else {
          w.write(OFF, 0, n);
          n = 0;
        }
      }
    }

    private final void closeElement() throws IOException {
      if (openElement) {
        w.write(">\n");
      }
      openElement = false;
    }

  }

  /**
   * A {@link org.xml.sax.ContentHandler ContentHandler} that splits XML
   * documents into smaller chunks. Each chunk is processed by the nested
   * {@link org.xml.sax.ContentHandler ContentHandler} obtained from
   * {@link java.net.ContentHandlerFactory ContentHandlerFactory}. This is
   * useful for running XSLT engine against large XML document that will hardly
   * fit into the memory all together.
   * <p>
   * TODO use complete path for subdocumentRoot
   */
  private static final class InputSlicingHandler extends DefaultHandler {
    private String subdocumentRoot;

    private final ContentHandler rootHandler;

    private ContentHandlerFactory subdocumentHandlerFactory;

    private boolean subdocument = false;

    private ContentHandler subdocumentHandler;

    /**
     * Constructs a new {@link InputSlicingHandler SubdocumentHandler} object.
     * 
     * @param subdocumentRoot
     *          name/path to the root element of the subdocument
     * @param rootHandler
     *          content handler for the entire document (subdocument envelope).
     * @param subdocumentHandlerFactory
     *          a {@link ContentHandlerFactory ContentHandlerFactory} used to
     *          create {@link ContentHandler ContentHandler} instances for
     *          subdocuments.
     */
    InputSlicingHandler(final String subdocumentRoot, final ContentHandler rootHandler, final ContentHandlerFactory subdocumentHandlerFactory) {
      this.subdocumentRoot = subdocumentRoot;
      this.rootHandler = rootHandler;
      this.subdocumentHandlerFactory = subdocumentHandlerFactory;
    }

    @Override
    public final void startElement(final String namespaceURI, final String localName, final String qName, final Attributes list) throws SAXException {
      if (subdocument) {
        subdocumentHandler.startElement(namespaceURI, localName, qName, list);
      } else if (localName.equals(subdocumentRoot)) {
        subdocumentHandler = subdocumentHandlerFactory.createContentHandler();
        subdocumentHandler.startDocument();
        subdocumentHandler.startElement(namespaceURI, localName, qName, list);
        subdocument = true;
      } else if (rootHandler != null) {
        rootHandler.startElement(namespaceURI, localName, qName, list);
      }
    }

    @Override
    public final void endElement(final String namespaceURI, final String localName, final String qName) throws SAXException {
      if (subdocument) {
        subdocumentHandler.endElement(namespaceURI, localName, qName);
        if (localName.equals(subdocumentRoot)) {
          subdocumentHandler.endDocument();
          subdocument = false;
        }
      } else if (rootHandler != null) {
        rootHandler.endElement(namespaceURI, localName, qName);
      }
    }

    @Override
    public final void startDocument() throws SAXException {
      if (rootHandler != null) {
        rootHandler.startDocument();
      }
    }

    @Override
    public final void endDocument() throws SAXException {
      if (rootHandler != null) {
        rootHandler.endDocument();

      }
    }

    @Override
    public final void characters(final char[] buff, final int offset, final int size) throws SAXException {
      if (subdocument) {
        subdocumentHandler.characters(buff, offset, size);
      } else if (rootHandler != null) {
        rootHandler.characters(buff, offset, size);
      }
    }

  }

  /**
   * A {@link org.xml.sax.ContentHandler ContentHandler} that splits XML
   * documents into smaller chunks. Each chunk is processed by the nested
   * {@link org.xml.sax.ContentHandler ContentHandler} obtained from
   * {@link java.net.ContentHandlerFactory ContentHandlerFactory}. This is
   * useful for running XSLT engine against large XML document that will hardly
   * fit into the memory all together.
   * 
   * <p>
   * TODO use complete path for subdocumentRoot
   */
  private static final class OutputSlicingHandler extends DefaultHandler {
    private final String subdocumentRoot;

    private ContentHandlerFactory subdocumentHandlerFactory;

    private final EntryElement entryElement;

    private boolean isXml;

    private boolean subdocument = false;

    private ContentHandler subdocumentHandler;

    /**
     * Constructs a new {@link OutputSlicingHandler SubdocumentHandler} object.
     * 
     * @param subdocumentHandlerFactory
     *          a {@link ContentHandlerFactory ContentHandlerFactory} used to
     *          create {@link ContentHandler ContentHandler} instances for
     *          subdocuments.
     * @param entryElement
     *          TODO.
     * @param isXml
     *          TODO.
     */
    OutputSlicingHandler(final ContentHandlerFactory subdocumentHandlerFactory, final EntryElement entryElement, final boolean isXml) {
      this.subdocumentRoot = "class";
      this.subdocumentHandlerFactory = subdocumentHandlerFactory;
      this.entryElement = entryElement;
      this.isXml = isXml;
    }

    @Override
    public final void startElement(final String namespaceURI, final String localName, final String qName, final Attributes list) throws SAXException {
      if (subdocument) {
        subdocumentHandler.startElement(namespaceURI, localName, qName, list);
      } else if (localName.equals(subdocumentRoot)) {
        String name = list.getValue("name");
        if (name == null || name.length() == 0) {
          throw new SAXException("Class element without name attribute.");
        }
        try {
          entryElement.openEntry(isXml ? name + ".class.xml" : name + ".class");
        } catch (IOException ex) {
          throw new SAXException(ex.toString(), ex);
        }
        subdocumentHandler = subdocumentHandlerFactory.createContentHandler();
        subdocumentHandler.startDocument();
        subdocumentHandler.startElement(namespaceURI, localName, qName, list);
        subdocument = true;
      }
    }

    @Override
    public final void endElement(final String namespaceURI, final String localName, final String qName) throws SAXException {
      if (subdocument) {
        subdocumentHandler.endElement(namespaceURI, localName, qName);
        if (localName.equals(subdocumentRoot)) {
          subdocumentHandler.endDocument();
          subdocument = false;
          try {
            entryElement.closeEntry();
          } catch (IOException ex) {
            throw new SAXException(ex.toString(), ex);
          }
        }
      }
    }

    @Override
    public final void startDocument() throws SAXException {
    }

    @Override
    public final void endDocument() throws SAXException {
    }

    @Override
    public final void characters(final char[] buff, final int offset, final int size) throws SAXException {
      if (subdocument) {
        subdocumentHandler.characters(buff, offset, size);
      }
    }

  }

  private static interface EntryElement {

    OutputStream openEntry(String name) throws IOException;

    void closeEntry() throws IOException;

  }

  private static final class SingleDocElement implements EntryElement {
    private final OutputStream os;

    SingleDocElement(final OutputStream os) {
      this.os = os;
    }

    public OutputStream openEntry(final String name) throws IOException {
      return os;
    }

    public void closeEntry() throws IOException {
      os.flush();
    }

  }

  private static final class ZipEntryElement implements EntryElement {
    private ZipOutputStream zos;

    ZipEntryElement(final ZipOutputStream zos) {
      this.zos = zos;
    }

    public OutputStream openEntry(final String name) throws IOException {
      ZipEntry entry = new ZipEntry(name);
      zos.putNextEntry(entry);
      return zos;
    }

    public void closeEntry() throws IOException {
      zos.flush();
      zos.closeEntry();
    }

  }

}
